tanstack query
1. 全局注入
import {creatRoot} from 'react-dom/client'
import App from './App.tsx'
import {QueryClient,QueryClientProvider} from '@tanstack/react-query'
const queryClient = new QueryClient();
creatRoot(document.getElementById('root')).render(
<QueryClientProvider client={queryClient}>
<App/>
</QueryClientProvider>
)2. useQuery基本使用
// 测试tanstack query
'use client'
import { useQuery } from "@tanstack/react-query";
const Test = () => {
const { data, isPending, refetch } = useQuery({
queryKey: ['test'],
queryFn: async () => {
const res = await new Promise((resolve, reject) => {
setTimeout(() => {
resolve('test')
}, 1000)
})
return res
}
})
return (
<div>
{isPending && <div>loading...</div>}
{JSON.stringify(data)}
<button onClick={() => refetch()}>refetch</button>
</div>
)
}
export default Test- 带请求参数用法:
// 测试tanstack query
'use client'
import { useQuery } from "@tanstack/react-query";
import { useState } from "react";
const Test = () => {
// 模拟请求参数
const [id, setId] = useState(1)
// 请求函数
const queryFn = async (id: number) => {
const res = await new Promise((resolve, reject) => {
setTimeout(() => {
resolve('test' + id)
}, 1000)
})
return res
}
const { data, isPending, refetch } = useQuery({
queryKey: ['test', id],
queryFn: () => queryFn(id)
})
return (
<div>
{isPending && <div>loading...</div>}
{JSON.stringify(data)}
<br />
<button onClick={() => refetch()}>refetch</button>
<br />
<button onClick={() => setId((pre) => pre + 1)}>id+1</button>
</div>
)
}
export default Test- 为了解决传统react应用useEffect无法写进条件判断的问题 无法根据布尔值来判断是否调接口 useQuery有一个配置
const [on,setOn] = useState(true)
const { data, isPending, refetch } = useQuery({
queryKey: ['test', id],
queryFn: () => queryFn(id),
enabled:on
})3.useSuspenseQuery悬念查询 与suspence
- 悬念查询
- 在ts中 默认解构出的data是any||unknow类型 如果知道返回的数据结构 可以通过类型来约束data
const getTodos = async ():Promise<Todo[]>=>{
....
return res
}
type Todo = {
userId:number,
id:number,
title:string,
completed:boolean
}- 如果使用ts 如以上todos案例 useQuery的返回值加了类型约束后 返回值data类型可能为todo[] || undefined,这时如果在模板中使用 data[0]?.title ts就会报错 这时就需要用到useSuspenseQuery 他的返回值是todo[]
//悬念查询
//可以保证返回的数据是一种可执行类型 而不是与undefined的联合类型
//适合处理想确保数据始终是定义的 并且不存在未定义的情况
const {data} = useSuspenseQuery(....)
return(
<div>
{data[0]?.title}
</div>
)- 与react.Suspence联合使用
如果子组件使用了useSuspenseQuery 父组件引入子组件并且将子组件包裹在Suspense标签中
那么当query未完成 组件会渲染suspense标签的fallback
function App(){
return (
<Suspense fallback={<Loading/>}>
<Son/>
</Suspense>
)
}
function Son(){
const {data} = useSuspenseQuery(....)
return(
<div>
{data[0]?.title}
</div>
)
}- 悬念查询的一个缺点是不能使用enabled配置来模拟条件查询 如果用条件查询的话最好用query
4.query配置的抽离
如果一个页面的请求比较多 那么useQuery和async函数的代码会堆积在组件中 这时可以将其抽离出来
创建于给单独的文件
import {queryOptions} from "@tanstack/react-query";
export default function createTodoQueryOptions(){
return queryOptions({
queryKey:['todos'],
queryFn:getTodos,
})
}
const getTodos = async ():Promise<Todo[]> => {
const response = await fetch(...)
return response.json();
}
type Todo = {
userID:number;
id:number;
...
}5. 同时进行多个查询
如果要同时进行多个查询 那么可以使用useQueries
const [{data},result2,result3] = useQueries({
queries:[
createTodoQueryOptions(),
createUsersQueryOptions(),
createPostQueryOptions()
]
})
//同样也有suspenseQueries
const [{data},result2,result3] = useSuspenseQueries({
queries:[
createTodoQueryOptions(),
createUsersQueryOptions(),
createPostQueryOptions()
]
})6. 查询依赖上一个查询
比如需求是获取用户id然后获取用户详情
那么第一个查询获取用户 第二个查询通过条件判断也就是enabled属性 来判断第二个查询是否执行
const {data:users} = useQuery(createUserQueryOptions());
const userId = user.id
//这里将获取到的用户id通过!!转为布尔类型来判断第二个查询是否自动执行----------------------
const {data:posts} = useQuery({...createPostQueryOptions,enabled:!!userId})
//第二种方法是第一个查询使用悬念查询 这样可以保证返回的data是定义的------------------------
const {data:users} = useSuspenseQuery(createUserQueryOptions());
const userId = user.id
const {data:posts} = useQuery(createPostQueryOptions)
//第二种方法的工作原理------------------------------------------------------------------
function Component() {
// 1. 第一次渲染:users 还在加载
const {data: users} = useSuspenseQuery(...)
// ⬆️ 这里会抛出 Promise,组件暂停渲染
// ❌ 下面的代码不会执行
const userId = users.id
const {data: posts} = useQuery(...)
}
// 2. users 加载完成后,组件重新渲染
function Component() {
const {data: users} = useSuspenseQuery(...)
// ✅ 现在 users 有值了
const userId = users.id // 可以直接访问
const {data: posts} = useQuery(...) // 现在才执行
}本质: useSuspenseQuery 把异步变成了”同步”的写法,通过 Suspense 机制阻塞组件渲染。
7. 轮询
const { data: videos, isLoading, error } = useQuery<Video[]>({
queryKey: ["videos"],
queryFn: async () => {
const res = await request.get("/videos");
return res.data;
},
// 智能轮询:仅当有视频处于“处理中”状态时才轮询,否则停止
refetchInterval: (query) => {
const data = query.state.data;
if (Array.isArray(data) && data.some((v: Video) => v.status === 'processing')) {
return 3000;//time
}
return false;
}
});8. useMutation
const deleteMutation = useMutation({
mutationFn: id => axios.delete(`/users/${id}`),
onSuccess: () => queryClient.invalidateQueries(["users"])
});
deleteMutation.mutate(userId);
什么时候选 mutateAsync?
后续操作 依赖后端返回值:
(1) 登录
const res = await loginMutation.mutateAsync(form);
localStorage.setItem("token", res.token);
router.push("/home");(2) 创建成功后跳转到详情页
const user = await createUserMutation.mutateAsync(form);
router.push(`/user/${user.id}`);(3) 多个操作串联
const saved = await saveMutation.mutateAsync(form);
await uploadMutation.mutateAsync(saved.id);(4) 需要 try/catch 捕获错误
try {
await updateMutation.mutateAsync(data);
} catch (err) {
alert("出错了");
}用一个例子串起来
const queryClient = useQueryClient();
const todoQuery = useQuery({
queryKey: ["todos"],
queryFn: () => axios.get("/todos")
});
const addTodoMutation = useMutation({
mutationFn: (newTodo) => axios.post("/todos", newTodo),
onSuccess: () => {
// 让列表重新刷新
queryClient.invalidateQueries(["todos"]);
}
});
// 点击提交按钮
const handleSubmit = () => {
addTodoMutation.mutate({ title: "新任务" });
};流程:
- 组件挂载 → useQuery 自动获取 todos
- 用户点提交 → mutate 手动触发 POST
- POST 成功 → onSuccess → invalidateQueries
- useQuery 自动重新发请求
- 页面自动显示最新的 todos 列表
你完全不需要写 “刷新列表”。
数据同步机制
React Query 的数据同步机制
核心原理:共享的全局缓存
React Query 通过一个全局的 QueryClient 实例管理所有查询缓存,所有组件共享这个缓存。
工作流程
┌─────────────────────────────────────────────────────────────┐
│ QueryClient (全局单例) │
│ ┌───────────────────────────────────────────────────────┐ │
│ │ 内存缓存 (In-Memory Cache) │ │
│ │ ┌───────────────────────────────────────────────┐ │ │
│ │ │ ['todos', '2025-01-15'] → { data: [...], ... }│ │ │
│ │ │ ['todos', '2025-01-16'] → { data: [...], ... }│ │ │
│ │ │ ['monthStatus', 2025, 1] → { data: {...}, ... }│ │ │
│ │ └───────────────────────────────────────────────┘ │ │
│ └───────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘
▲ ▲
│ │
┌────┴────┐ ┌────┴────┐
│ │ │ │
TodosDropdown Todos Page
(在 Layout 中) (独立页面)详细步骤
1. 初始化阶段(应用启动)
// providers.tsx
const queryClient = new QueryClient() // ← 创建全局单例
<QueryClientProvider client={queryClient}> // ← 注入到整个应用
{children}
</QueryClientProvider>2. TodosDropdown 组件(Layout 中)
// todos-dropdown.tsx (第 26-30 行)
const { data: todos } = useQuery({
queryKey: ['todos', today], // ← 查询键:['todos', '2025-01-15']
queryFn: () => todosApi.getTodosByDate(today),
})
// 双击切换完成状态 (第 33-46 行)
const toggleMutation = useMutation({
mutationFn: (id: string) => todosApi.toggleComplete(id),
onSuccess: () => {
// 🔑 关键:标记缓存为"过期"
queryClient.invalidateQueries({ queryKey: ['todos', today] })
queryClient.invalidateQueries({ queryKey: ['todos'] }) // 所有 todos
},
})3. Todos 页面组件
// todos/page.tsx (第 35-38 行)
const { data: todos = [] } = useQuery({
queryKey: ['todos', dateStr], // ← 如果 dateStr === today,使用相同的缓存!
queryFn: () => todosApi.getTodosByDate(dateStr),
})同步过程
步骤 1: 用户在 Layout 中双击完成任务
↓
步骤 2: toggleMutation.mutate() 调用 API
↓
步骤 3: API 返回成功,触发 onSuccess
↓
步骤 4: queryClient.invalidateQueries(['todos', today])
↓
步骤 5: QueryClient 标记缓存为"过期" (stale)
↓
步骤 6: React Query 检测到缓存过期
↓
步骤 7: 自动触发所有使用该查询键的 useQuery 重新获取数据
↓
步骤 8: Todos 页面的 useQuery 自动重新调用 API
↓
步骤 9: 新数据更新到缓存,组件自动重新渲染关键点
- 共享缓存:所有组件通过
QueryClientProvider共享同一个QueryClient实例 - 查询键匹配:使用相同的
queryKey会共享同一份缓存数据 - 自动失效:
invalidateQueries会标记匹配的查询为过期 - 自动重新获取:
useQuery会监听缓存状态,过期时自动重新获取 - 无需 WebSocket:这是客户端内存缓存机制,不依赖网络推送
代码验证
可以在浏览器控制台验证:
// 在浏览器控制台执行
import { useQueryClient } from '@tanstack/react-query'
// 查看当前缓存
const queryClient = useQueryClient()
console.log(queryClient.getQueryCache().getAll())总结
- 不需要 WebSocket:这是客户端内存缓存同步
- 不需要手动刷新:React Query 自动处理
- 性能优化:相同查询键的数据会被缓存和复用
- 实时同步:任何组件调用
invalidateQueries,所有相关组件都会自动更新
这就是为什么在 Layout 中修改数据后,Todos 页面会自动更新的原因。
Last updated on